fix: support Text selectable prop on macOS with Fabric#2845
fix: support Text selectable prop on macOS with Fabric#2845kessenma wants to merge 1 commit intomicrosoft:mainfrom
Conversation
…ure)
The `selectable` prop on `<Text>` was completely ignored on macOS when
using Fabric. The `isSelectable` handling in `updateProps` was gated
behind `#if !TARGET_OS_OSX`, so on macOS no selection support was enabled.
This adds macOS text selection support to `RCTParagraphComponentView` by
using an `NSTextView` overlay (similar to the old architecture's approach
in `RCTTextView.mm`). When `selectable={true}`:
- An `NSTextView` is created and added as a subview for selection handling
- The `NSTextView`'s text storage is synced from `RCTTextLayoutManager`
- `RCTParagraphTextView` skips its own drawing to avoid double-rendering
- Mouse events (click-drag, double/triple-click, right-click) are
forwarded to the `NSTextView` for native selection behavior
When `selectable={false}` (default), behavior is unchanged — no
`NSTextView` is created and rendering works exactly as before.
Fixes microsoft#2844
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
|
@microsoft-github-policy-service agree |
|
Curious, did you intend this to be a copy of #2673 ? Did you do any testing to make sure it works? |
|
I made this PR because I noticed I was not able to copy text in the latest version of the repo on my personal project and yes I did test. I put this in the PR description Tested in RNTester macOS app: |
|
Hello! I like the changes in this branch, but there are some things I want to do slightly differently. Namely, adding iOS as well so this can maybe get pushed upstream to React Native too. I made #2864 based on both my old and your changes. If that merges, I'll close this PR. Sound good? |
|
Closing in favor of #2864 . I'll try to add a coauthor note before merging so credit stays there :) |
…ue} (#2864) Followup from #2845 ## Summary - Implements native text selection support for `<Text selectable={true}>` in the Fabric (new architecture) renderer - On **macOS**, swaps the content view to an `NSTextView` subclass that handles click-drag, double-click (word), and triple-click (line) selection, plus right-click context menus - On **iOS**, swaps to a `UITextView` subclass that leverages built-in gesture recognizers for long-press-to-select - The selectable text view is created lazily — only when `selectable={true}` — so there is zero overhead for non-selectable text (the common case) - Ungates `RCTTextLayoutManager.getTextStorageForAttributedString:` so both platforms can sync Fabric's attributed string into the native text view | iOS | macOS | | ----------- | ----------- | | <img width="506" height="960" alt="Screenshot 2026-03-20 at 11 30 06 PM" src="https://github.com/user-attachments/assets/ac7ce231-0969-4003-a3ca-55fae4e35515" /> | <img width="805" height="868" alt="Screenshot 2026-03-20 at 11 36 36 PM" src="https://github.com/user-attachments/assets/9cc0e951-8bff-41a5-92ff-1f3589ab7279" /> | ## Approach Rather than adding selection logic to the existing `RCTParagraphTextView`, this introduces a separate `RCTParagraphSelectableTextView` (platform-native text view) that is swapped in as the content view when the `selectable` prop is set. This keeps the non-selectable path untouched and avoids runtime cost for the default case. On macOS, mouse events are intercepted at the `RCTParagraphComponentView` level to distinguish single clicks (forwarded to JS for `onPress`) from drag/double-click/triple-click gestures (forwarded to the `NSTextView` for native selection). Touch cancellation walks the view hierarchy to toggle `RCTSurfaceTouchHandler` without modifying that class. On iOS, `UITextView` handles selection natively through its built-in gesture recognizers — no custom hit-testing needed. ## Test plan Add the following to RNTesterPlayground and verify on both macOS and iOS: ```jsx import {Alert, StyleSheet, Text, View} from 'react-native'; function Playground() { return ( <View style={styles.container}> <Text style={styles.heading}>Text Selection Test</Text> <Text style={styles.label}>Selectable text (try click-drag, double-click, right-click):</Text> <Text selectable={true} style={styles.selectableText}> This text should be selectable. Try clicking and dragging to select a range of text. Double-click to select a word. Triple-click to select a line. Right-click to see the context menu. </Text> <Text style={styles.label}>Non-selectable text (default):</Text> <Text style={styles.nonSelectableText}> This text should NOT be selectable. Clicking and dragging should not create a text selection. This is the default behavior. </Text> <Text style={styles.label}>Selectable with nested styles:</Text> <Text selectable={true} style={styles.selectableText}> This has <Text style={styles.bold}>bold text</Text> and{' '} <Text style={styles.italic}>italic text</Text> and{' '} <Text style={styles.colored}>colored text</Text> inside it. Selection should work across all styled ranges. </Text> <Text style={styles.label}>Selectable with onPress (should not conflict):</Text> <Text selectable={true} onPress={() => Alert.alert('Text pressed!')} style={styles.pressableText}> This text is both selectable and pressable. A single click should trigger onPress. Click-drag should start a selection instead. </Text> </View> ); } ``` - [ ] Verify `<Text selectable={true}>` enables click-drag selection on macOS - [ ] Verify double-click selects a word, triple-click selects a line on macOS - [ ] Verify right-click shows native context menu with Copy on macOS - [ ] Verify `<Text selectable={true}>` enables long-press selection on iOS - [ ] Verify non-selectable text (default) is unchanged on both platforms - [ ] Verify `onPress` still fires for single clicks on selectable text - [ ] Verify nested styled text renders correctly when selectable - [ ] Verify selection is cleared when text loses focus 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Kyle Essenmacher <15271436+kessenma@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
…ue} (microsoft#2864) Followup from microsoft#2845 ## Summary - Implements native text selection support for `<Text selectable={true}>` in the Fabric (new architecture) renderer - On **macOS**, swaps the content view to an `NSTextView` subclass that handles click-drag, double-click (word), and triple-click (line) selection, plus right-click context menus - On **iOS**, swaps to a `UITextView` subclass that leverages built-in gesture recognizers for long-press-to-select - The selectable text view is created lazily — only when `selectable={true}` — so there is zero overhead for non-selectable text (the common case) - Ungates `RCTTextLayoutManager.getTextStorageForAttributedString:` so both platforms can sync Fabric's attributed string into the native text view | iOS | macOS | | ----------- | ----------- | | <img width="506" height="960" alt="Screenshot 2026-03-20 at 11 30 06 PM" src="https://github.com/user-attachments/assets/ac7ce231-0969-4003-a3ca-55fae4e35515" /> | <img width="805" height="868" alt="Screenshot 2026-03-20 at 11 36 36 PM" src="https://github.com/user-attachments/assets/9cc0e951-8bff-41a5-92ff-1f3589ab7279" /> | ## Approach Rather than adding selection logic to the existing `RCTParagraphTextView`, this introduces a separate `RCTParagraphSelectableTextView` (platform-native text view) that is swapped in as the content view when the `selectable` prop is set. This keeps the non-selectable path untouched and avoids runtime cost for the default case. On macOS, mouse events are intercepted at the `RCTParagraphComponentView` level to distinguish single clicks (forwarded to JS for `onPress`) from drag/double-click/triple-click gestures (forwarded to the `NSTextView` for native selection). Touch cancellation walks the view hierarchy to toggle `RCTSurfaceTouchHandler` without modifying that class. On iOS, `UITextView` handles selection natively through its built-in gesture recognizers — no custom hit-testing needed. ## Test plan Add the following to RNTesterPlayground and verify on both macOS and iOS: ```jsx import {Alert, StyleSheet, Text, View} from 'react-native'; function Playground() { return ( <View style={styles.container}> <Text style={styles.heading}>Text Selection Test</Text> <Text style={styles.label}>Selectable text (try click-drag, double-click, right-click):</Text> <Text selectable={true} style={styles.selectableText}> This text should be selectable. Try clicking and dragging to select a range of text. Double-click to select a word. Triple-click to select a line. Right-click to see the context menu. </Text> <Text style={styles.label}>Non-selectable text (default):</Text> <Text style={styles.nonSelectableText}> This text should NOT be selectable. Clicking and dragging should not create a text selection. This is the default behavior. </Text> <Text style={styles.label}>Selectable with nested styles:</Text> <Text selectable={true} style={styles.selectableText}> This has <Text style={styles.bold}>bold text</Text> and{' '} <Text style={styles.italic}>italic text</Text> and{' '} <Text style={styles.colored}>colored text</Text> inside it. Selection should work across all styled ranges. </Text> <Text style={styles.label}>Selectable with onPress (should not conflict):</Text> <Text selectable={true} onPress={() => Alert.alert('Text pressed!')} style={styles.pressableText}> This text is both selectable and pressable. A single click should trigger onPress. Click-drag should start a selection instead. </Text> </View> ); } ``` - [ ] Verify `<Text selectable={true}>` enables click-drag selection on macOS - [ ] Verify double-click selects a word, triple-click selects a line on macOS - [ ] Verify right-click shows native context menu with Copy on macOS - [ ] Verify `<Text selectable={true}>` enables long-press selection on iOS - [ ] Verify non-selectable text (default) is unchanged on both platforms - [ ] Verify `onPress` still fires for single clicks on selectable text - [ ] Verify nested styled text renders correctly when selectable - [ ] Verify selection is cleared when text loses focus 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Kyle Essenmacher <15271436+kessenma@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Summary
Fixes #2844
The
selectableprop on<Text>was completely ignored on macOS when using Fabric (New Architecture). TheisSelectablehandling inRCTParagraphComponentView.updateProps:was gated behind#if !TARGET_OS_OSX, so on macOS no selection support was ever enabled.This adds macOS text selection support by using an
NSTextViewfor both rendering and selection (matching the pattern used by the old architecture inRCTTextView.mm):RCTParagraphTextViewskips itsdrawRect:and theNSTextViewhandles all text rendering via its ownNSLayoutManagerselectable={false}(default), noNSTextViewis created and behavior is completely unchangedNSTextViewis torn down inprepareForRecycleKey implementation details
RCTParagraphSelectionTextView(NSTextView subclass withcanBecomeKeyView = NO) prevents double-focusRCTTextLayoutManager.getTextStorageForAttributedString:(existing macOS-only API)hitTest:,mouseDown:,rightMouseDown:) ported from old arch'sRCTTextView.mmresignFirstResponderprevents losing focus during active selection trackingTest plan
#if TARGET_OS_OSXguarded)🤖 Generated with Claude Code